package buru.server.netty;


import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufOutputStream;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.*;
import org.glassfish.jersey.internal.MapPropertiesDelegate;
import org.glassfish.jersey.server.*;
import org.glassfish.jersey.server.internal.ConfigHelper;
import org.glassfish.jersey.server.internal.ContainerUtils;
import org.glassfish.jersey.server.spi.Container;
import org.glassfish.jersey.server.spi.ContainerLifecycleListener;
import org.glassfish.jersey.server.spi.ContainerResponseWriter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.ws.rs.core.SecurityContext;
import java.io.OutputStream;
import java.net.URI;
import java.security.Principal;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import static io.netty.handler.codec.http.HttpHeaderNames.HOST;

/**
 * Auth: bruce-sha
 * Date: 2015/3/31
 */
@ChannelHandler.Sharable
public class NettyHandlerJerseyContainer extends SimpleChannelInboundHandler<HttpRequest> implements Container {

    public static final String PROPERTY_CONTEXT_PATH = "com.sun.jersey.server.impl.container.netty.contextPath";

    volatile private ApplicationHandler applicationHandler;
    volatile private ContainerLifecycleListener containerListener;

    final private String contextPath;

    public NettyHandlerJerseyContainer(ResourceConfig resourceConfig) {
        this(new ApplicationHandler(resourceConfig));
    }

    public NettyHandlerJerseyContainer(ApplicationHandler appHandler) {
        this.applicationHandler = appHandler;
        this.containerListener = ConfigHelper.getContainerLifecycleListener(appHandler);
        Object configedContextPath = appHandler.getConfiguration().getProperty(PROPERTY_CONTEXT_PATH);
        this.contextPath = configedContextPath == null ? "" : configedContextPath.toString();
    }

    @Override
    public ApplicationHandler getApplicationHandler() {
        return applicationHandler;
    }

    @Override
    public ResourceConfig getConfiguration() {
        return applicationHandler.getConfiguration();
    }


    private final static class Writer implements ContainerResponseWriter {
        private final ChannelHandlerContext context;
        private DefaultFullHttpResponse response;

        private Writer(ChannelHandlerContext context) {
            this.context = context;
        }

        @Override
        public OutputStream writeResponseStatusAndHeaders(long contentLength, ContainerResponse responseContext)
                throws ContainerException {
            final ByteBuf contentBuf = Unpooled.buffer(Long.valueOf(contentLength).intValue());
            final HttpResponseStatus status = HttpResponseStatus.valueOf(responseContext.getStatus());

            response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, contentBuf);

            response.headers().add(HttpHeaderNames.CONTENT_LENGTH, Long.toString(contentLength));

            for (Map.Entry<String, List<String>> e : responseContext.getStringHeaders().entrySet()) {
                response.headers().add(e.getKey(), e.getValue());
            }
            return new ByteBufOutputStream(contentBuf);
        }

        @Override
        public boolean suspend(long l, TimeUnit timeUnit, TimeoutHandler timeoutHandler) {
            throw new UnsupportedOperationException("Method suspend is not supported by the container.");
        }

        @Override
        public void setSuspendTimeout(long l, TimeUnit timeUnit) throws IllegalStateException {
            throw new UnsupportedOperationException("Method suspend is not supported by the container.");
        }

        @Override
        public void commit() {
            //http 1.1默认connection: keep-alive
            //if (HttpHeaderUtil.isKeepAlive(request)) {
            //    response.headers().set(CONNECTION, HttpHeaderValues.KEEP_ALIVE);
            //    context.write(response);
            //}
            context.write(response).addListener(ChannelFutureListener.CLOSE);
        }

        @Override
        public void failure(Throwable throwable) {
            try {
                commit();
            } finally {
                log.error("ContainerResponseWriter error:", throwable);
                context.close();
                rethrow(throwable);
            }
        }

        /**
         * Rethrow the original exception as required by JAX-RS, 3.3.4
         */
        private void rethrow(final Throwable error) {
            if (error instanceof RuntimeException) {
                throw (RuntimeException) error;
            } else {
                throw new ContainerException(error);
            }
        }

        @Override
        public boolean enableResponseBuffering() {
            return true;
        }
    }

    private SecurityContext getSecurityContext(final HttpRequest request) {
        // TODO This is major. Figure out what this does, and how to implement it properly.
        // I'm guessing security-stuff such as basic-auth won't work without it.
        return new SecurityContext() {
            @Override
            public Principal getUserPrincipal() {
                return null;
            }

            @Override
            public boolean isUserInRole(String s) {
                return false;
            }

            @Override
            public boolean isSecure() {
                return false;
            }

            @Override
            public String getAuthenticationScheme() {
                return null;
            }
        };
    }

    @Override
    public void messageReceived(ChannelHandlerContext context, HttpRequest request) throws Exception {

        final String uri = request.uri();

        final URI baseUri = new URI(getBaseUri(request));
        final URI requestUri = baseUri.resolve(ContainerUtils.encodeUnsafeCharacters(uri));

        try {
            ContainerRequest containerRequest = new ContainerRequest(
                    baseUri,
                    requestUri,
                    request.method().name().toString(),
                    getSecurityContext(request),
                    new MapPropertiesDelegate());

            containerRequest.setWriter(new Writer(context));

            applicationHandler.handle(containerRequest);

        } finally {
            context.flush();
            context.close();
        }
    }

    private String getBaseUri(HttpRequest request) {
        return String.format("http://%s/%s/", request.headers().get(HOST), contextPath);
    }

    @Override
    public void reload() {
        reload(getConfiguration());
    }

    @Override
    public void reload(ResourceConfig resourceConfig) {
        containerListener.onShutdown(this);
        applicationHandler = new ApplicationHandler(resourceConfig);
        containerListener = ConfigHelper.getContainerLifecycleListener(applicationHandler);
        containerListener.onReload(this);
        containerListener.onStartup(this);
    }

    static private final Logger log = LoggerFactory.getLogger(NettyHandlerJerseyContainer.class);
}